搜索 K
Appearance
博客正在加载中...
Appearance
我们从计算机的启动开始,讲解开机的时候,计算机内部到底发生了什么。这是大家每次开机都会看到的画面,也是第一幅画面,从这里开始合情合理;并且安排了实验一,控制计算机的启动。
打开电源之后,计算机就开始工作了,那么计算机是如何工作的呢?
我们简单回顾下计组里的内容:计算机是在一个计算模型下设计出来的,也就是说计算机是计算模型的一种实现,最著名的模型就是图灵提出的图灵机(可以回顾计算机简史)。
图灵机是怎么定义的呢?实际上图灵机借鉴了人在计算的过程:例如我们计算 3+2,我们用眼睛看到 3+2 后,就知道要做加法,然后在脑海里运算,得到结果是 5,然后将答案写出。而图灵机也是这样,当图灵机在纸带上读到 3+2 这条指令后,就知道要做加法,并且用运算器 得出结果,并写回到纸带上。
计算机的工作原理无非 4 个字:取指执行。大家一定一定要牢记这 4 个字。CPU 从内存中取出指令,如果是加法指令,就执行加法; 如果是乘法指令,就执行乘法。这就是通用图灵机。同样的,如果我们将应用程序放到内存里,例如浏览器,那么 CPU 就运行浏览器;如果是 Word,CPU 就运行 Word。 一开始内存是空的,CPU 从哪里取指呢?第一条指令是多少?PC 指针的初值是多少?这个由硬件设计者决定的。
这就要了解一下硬件的知识。以 x86 结构为例,刚一上电的时候,内存中有一部分是固化的 ROM,叫做 ROM BIOS。BIOS 全称 Basic Input Output System,基本输入输出系统。也就是说,总得有一个基本的输入输出,如果什么都没有,CPU 是做不到取指执行的。那么这一段指令做了什么事情呢?
关于第 5 步的补充:在计组里我们学过,一个扇区是 512 个字节的。而 0 磁道 0 扇区就是操作系统的引导扇区,因此就是将引导扇区里的指令读到内存里,并开始执行。这也就是操作系统的第一段代码。
接下来我们会讲 Linux 的部分源码,同学们可以先准备下,接下来我们仅仅会挑部分重点代码进行解读。
Linux 0.11 版本的代码地址:http://oldlinux.org/Linux.old/Linux-0.11/sources/system/linux-0.11.tar.Z
也可以在我的 Gitee 上下载:https://gitee.com/peterjxl/learn-os
本段代码很小,不到 500k; 代码里也有详细的注释。
我们接下来讲的 bootsect.s,setup.s 和 head.s 的路径:linux-0.11\boot\。该目录下也就只有这几个文件
引导扇区里的代码:是一段汇编代码,文件名是 bootsect.s
为什么第一段代码是汇编代码呢?因此我们要对计算机进行精细的控制,而 C 语言的代码编译之后,它的内存位置是人为不可控的(比如自动分配栈)。而汇编就不一样了,汇编中的每一条指令最后都变成了这都变成了真正的机器指令,所以你可以对它进行完整的控制。而在引导过程中,你当然要对他进行完整的控制,绝对不能有任何差错或者细小的出入的控制。在操作系统中有很多地方都要实现这样精细的控制。
我们会挑 bootsect.s 的一些关键代码来解读。
INITSEG = 0x9000 ! we move boot here - out of the way SETUPSEG = 0x9020 ! setup starts here entry start start: mov ax,#BOOTSEG mov ds,ax mov ax,#INITSEG mov es,ax mov cx,#256 sub si,si sub di,di rep movw jmpi go,INITSEG !段间跳转 cs=INITSEG, ip=go go: mov ax,cs mov ds,ax mov es,ax
我们来解读这段代码
1 ~ 4 行可以理解为定义了一个变量,值是我们后续会用到的地址
第 5 行:关键字 entry 告诉链接器“程序入口” 从 start 标号开始
第 6 行:CPU 开始执行第 5 行的指令(上一句表明了程序的入口)
接下来执行完这两句后,ds = 0x07c0 :
```asm
mov ax,#BOOTSEG
mov ds,ax同理,执行完这两句后,es = 0x9000 :
mov ax,#INITSEG
接下来做什么呢?就是将 bootsect.s 里的代码,全部挪到内存 0x9000 处:使用的是 rep 指令,复制 256 个字,也就是 512 字节,刚好一个扇区,从 0x7c00 开始的一个扇区的代码。为什么要将本段代码移动到 9000,我们后续再讲
```asm
mov cx,#256
sub si,si
sub di,di
rep movw
接下来执行一个跳转指令:
```asm
jmpi go,INITSEG !段间跳转 cs=INITSEG, ip=go也就是将 0x9000 作为基址,go 作为偏移地址。我们可以推理出来,既然 bootsect.s 已经挪到 0x9000 处, 我们就应该跳转到 0x9000 处,继续往下执行 bootsect.s 处的代码,且我们可以看到 go 标号就在 跳转指令的下一行,所以其实就是继续往下执行 bootsect.s。
接下来 bootsect.s 做了什么事情呢?我们继续看一些关键的代码:
go: mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov sp,#0xFF00 ! arbitrary value >>512
load_setup:
mov dx,#0x0000 ! drive 0, head 0
mov cx,#0x0002 ! sector 2, track 0
mov bx,#0x0200 ! address = 512, in INITSEG
mov ax,#0x0200+SETUPLEN ! service 2, nr of sectors
int 0x13 ! read it
jnc ok_load_setup ! ok - continue
mov dx,#0x0000
mov ax,#0x0000 ! reset the diskette
int 0x13
jmp load_setup当执行到 go 处的代码时,CS = 9000,而 go 标号处 到 load_sectup 标号处的代码,将段寄存器 DS,ES,SS 的值都设置为了 9000。因此目前所有段寄存器的值都是 0x9000
下一步,就是用 0x13 号中断 ,将 setup 模块从磁盘加载到内存里。因为我们一开始只将引导扇区读入进来了,而操作系统还有很多内容要读进来。
我们来解读下 load_setup 代码:
在执行 13 号中断之前,我们要先传参给中断例程,其中
因此 load_setup 就是读取 4 个扇区的内容到 0x90200 处。十六进制是 200,转换成十进制就是 512,因此就是 sectup 模块在内存的地址就是紧挨着 bootsect.s 。内存示意图如下:
| 内存地址 | 内存的内容 |
|---|---|
| 0xFFFF0 | ROM BIOS 区 |
| .......... | ............. |
| 0x90200 | setup 模块 |
| 0x90000~0x901FF | bootsect.s 模块 |
| .............. | .................... |
读入 setup 模块后,接下来做什么呢?setup 模块只有 4 个扇区,操作系统当然不只这么少代码,因此读入了 setup 模块后,继续读入操作系统的 system 模块。我们来看看 ok_load_setup 的关键代码:
ok_load_setup:
mov ch,#0x00
mov sectors,cx
mov ax,#INITSEG
mov es,ax
mov ah,#0x03
mov bx,#0x0007
mov bp,#msg1
mov ax,#0x1301
int 0x10 ; ah功能为为13,作用是显示字符串,es:bp是串地址,cx是串长度
mov ax,#SYSSEG
mov es,ax ! segment of 0x010000
call read_it ; 这里就是读入system模块
jmpi 0,SETUPSEG ; 跳转到0x09020:0000处执行代码,也就是执行setup.s
; ...........这里省略一些代码, 以下这段代码在bootsect.s的244行.......
msg1:
.byte 13,10
.ascii "Loading system ..."
.byte 13,10,13,10 我们来分析下上述代码做了什么:
bootsect.s 做了以下事情:
具体代码执行逻辑:
通过汇编的 rep movw 指令,将自己的代码复制到 0x9000:0000 处 512 字节(256 个字,所以 cx=256)
执行跳转指令 jmp go 9000 (go 是标号,会赋值给 IP, 9000 会赋值给 CS,作为段基址)。其实就是顺序执行,只不过因为在第一步里将自己挪到了 9000,所以 CS:IP 也指到那里去
目前 bootset.s 的段基址是 9000,然后长度是 256 个字,引导扇区在内存里的结束地址是 90200H
然后根据 13 号中断,从第二个扇区开始(第一个扇区是 bootsect.s),读取 setup 的 4 个扇区的内容(al 存放扇区数量),也就是 2kb
至此,bootsect.s 结束
目前操作系统还在启动中,还需要精细的控制,因此 setup 也是一段汇编。首先我们根据文件名可以联想到:setup 应该是完成 OS 启动前的一些设置。我们还是挑部分重点代码来看。
mov ax,#INITSEG ; INITSEG 在setup.s里的第17行 定义为0x9000
mov ds,ax ; ds = 0x9000
mov ah,#0x03
xor bh,bh
int 0x10 ; 10号中断的3号功能 读光标位置
mov [0],dx ; 取出光标位置 放到0x9000处
mov ah,#0x88
int 0x15
mov [2],ax ; 获取扩展内存大小,并放到0x9000处
mov ah,#0x12
mov bl,#0x10
int 0x10 ; 获取根设备号 放到0x9000处
mov [8],ax
mov [10],bx
mov [12],cx
我们可以解读下这段代码:首先获取光标,内存,显卡等参数(后面还有很多代码获取硬件参数,这里不表),并放到 0x90000 处(此时 bootsect.s 已经不在用到,可以被覆盖)。完整的参数和保留的位置请见下表:(该表来自《Linux 内核完全剖析 第 6.3.1 节》)
|内存地址|长度(字节)|名称|描述|
| --------| ----------| ----------| --------------------------------------------------|
|0x90000|2|光标位置|列号(0x00-最左端),行号(0x00-最顶端)|
|0x90002|2|扩展内存数|系统从 1MB 开始的扩展内存数值(KB)。|
|0x90004|2|显示页面|当前显示页面|
|0x90006|1|显示模式||
|0x90007|1|字符列数||
|0x90008|2|??||
|0x9000A|1|显示内存|显示内存(0x00-64k,0x01- 128k,0x02- 192k,0x03=256k)|
|0x9000B|1|显示状态|0x00-彩色,I/O=0x3dX﹔ 0x01-单色,I/O=0x3bX|
|0x9000C|2|特性参数|显示卡特性参数|
|...||||
|0x90080|16|硬盘参数表|第 1 个硬盘的参数表|
|0x90090|16|硬盘参数表|第 2 个硬盘的参数表(如果没有,则清零)|
|0x901FC|2|根设备号|根文件系统所在的设备号(bootsec.s 中设置)|
其中,获取扩展内存数是非常重要的。
1. 什么是扩展内存:早期计算机中,地址总线只有 20 位,因此只能寻址 1M 以内的内存;而如今的计算机,都是 8G,16G 内存起步的,那么通常把 1M 以后的这些内存就叫扩展内存
2. 为什么获取内存很重要:操作系统,就是帮我们管理硬件的,而内存就是一个重要的硬件。要管理好内存,首先得知道内存的多大。
我们再谈谈,为什么这段代码的文件名是 setup.s ? 要管理好硬件,就得设置一些数据结构去保存这些信息。就好比学校里管理学生,就会有一个学生信息表来存储学生的信息,方便查询、修改和删除等;不仅仅是内存,还有光标,显卡参数等,获取完这些信息后先存起来,后面会形成一些数据结构来保存这些信息,这就是为什么叫 setup.s 。(实际上操作系统开机就做了 2 件事,第一件事就是读取操作系统到内存里,第二件事就是 setup,初始化,我们后面会慢慢体会到这句话)
### 第二件事:挪动 system 模块
在获取完硬件参数后的代码如下:
```asm
cli ; 不允许中断
mov ax,#0x0000
cld
do_move:
mov es,ax
add ax,#0x1000
cmp ax,#0x9000
jz end_move
mov ds,ax
sub di,di
sub si,si
mov cx,#0x8000
do_move,我们可以猜测是移动的意思,那么移动什么呢?
首先 ES 被置成 0;
在第一次循环中,DS = 1000,然后设置 cx,因此就是将 DS:SI 也就是 1000:0000 处的代码,挪到 ES:DI 也就是 0000:0000 处。而在 bootsect.s 里,1000:0 处的代码就是 system 模块。因此本段代码的作用就是循环,将 system 模块挪到 0 地址处。(每次复制 system 模块的 0x8000 个字节到 0 地址处,然后 ax 自增,判断是否复制完了,没有则继续循环)
之后,system 模块就会一直在 0 地址处。而 system 之后的内存,我们就可以用于运行我们自己的程序了,例如浏览器,Word 等。
### 第三件事:进入保护模式,将控制权交给操作系统
setup 模块该做的事都差不多了,接下来就是将控制权交给操作系统了,但在这之前,还有一件非常重要的事去做:进入保护模式。
我们知道,早期计算机只支持 1M 的内存,这指的是早期的寻址方式,只支持 1M。早期使用的是段基址 + 偏移地址这样的方式寻址的,这种方式不能满足 4G 内存的寻址,因此,我们要切换到一个新的寻址模式。因此 CPU 接下来会从 16 位寻址模式(也叫实模式)切换到 32 位寻址模式(也叫保护模式)。
那么 CPU 是怎么切换寻址模式呢?根据一个寄存器:CR0 。 如果这个寄存器的最后一位是 0,CPU 就会用 16 位模式;如果是 1,就用保护模式(其实就是换一条电路去寻址)。
```asm
mov ax,#0x0001
lmsw ax
jmpi 0,8在 setup.s 第 192 行,有个 lmsw ax,就是将 ax 的值赋给 CR0 寄存器,然后接下来的指令,就是用 32 位寻址模式了。 那么 32 位寻址模式怎么寻址呢?这就要提到一个非常著名概念叫 GDT(全局描述表 Global Descriptor Table),GDT 表里面存放的才是基址。 当然这也是硬件帮我们实现的寻址方式(因为硬件快)。如何用 GDT 寻址?
在 16 位模式下,代码寻址是用 CS:IP 实现的,而在 32 位模式下,CS 不再左移 4 位产生一个地址,而是用作选择子,换句话说就是 CS 的内容是 GDT 表的下标,对应的 GDT 表项的内容,才是段基址。
因此,32 位寻址模式是这样工作的:首先根据 CS 取出 GDT 表的内容作为基址,IP 还是作为偏移地址,因此来产生一个新的地址,示意图:
同样的,保护模式下,中断例程的寻址方式也发生了变化:仿照 GDT 表,新建了一个 IDT 表(中断描述符表 Interrupt Descriptor Table),int n 就用 n 进行查表取出中断例程的地址,然后执行:
gdt: .word 0,0,0,0 ! dummy
.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)
.word 0x0000 ! base address=0
.word 0x9A00 ! code read/exec
.word 0x00C0 ! granularity=4096, 386
.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)
.word 0x0000 ! base address=0
.word 0x9200 ! data read/write
.word 0x00C0 ! granularity=4096, 386
idt_48: .word 0 ! idt limit=0 .word 0,0 ! idt base=0L gdt_48: .word 0x800 ! gdt limit=2048, 256 GDT entries .word 512+gdt,0x9 ! gdt base = 0X9xxxx
我们可以看到有很多 word 指令,一个 word 就是 16 位,而 GDT 表一个表项占 8 字节(64 位),因此每 4 个 word 就是 一个 GDT 的表项(其中,第一个表项为空不使用)
每个表项的组成如下:

而 GTD 的下标如何确定呢?依次为 0,8,16………… 我们以 setup.s 的表为例:
|GDT 表下标|GDT 表内容|
| ----------| -----------------------------------------|
|16|.word 0x07FF 0x0000 0x9200 0x00C0|
|8|.word 0x07FF 0x0000 0x9A00 0x00C0|
|0|.word 0,0,0,0|
GDT 的相关部分我们介绍完了,我们回到本小结的开始 ,`jmpi 0,8` 这条指令是如何确定跳转到哪呢?
```asm
mov ax,#0x0001
lmsw ax
jmpi 0,8由于 CS 是 8,因此我们去查 8 这个下标的内容:
.word 0x07FF 0x0000 0x9A00 0x00C0 而这几个 word 是如何存放到 GDT 表的呢?
.word 0x07FF 0x0000 0x9A00 0x00C0
在内存中,从高地址到 低地址极速 0x00C0 9A00 0000 07FF
用二进制展开来就是
0x00C0: 0000 0000 1100 0000
0x9A00: 1001 1010 0000 0000
0x0000: 0000 0000 0000 0000
0x07FF: 0000 0111 1111 1111因此,我们可以这样将 07FF 放到 0~15 的地方,然后 0x0000 放到 16 ~ 31 的地方,然后 0x9A00 的后两个字节,00,放到 16~23 这个地方
放完后,我们可以看到,段基址就是 0。因此,jmpi 0,8 其实就是跳到 0 地址处去执行。至此,set up 的工作到此就完成。
setup 做的事情小结:
补充两个知识点:
system 模块的第一个部分是 head.s ,head.s 做了什么呢?
我们可以看看其关键的代码:还是初始化了 GDT 和 IDT 表(之前的 setup.s 里建立的 GDT 只是临时用于跳转而已),现在操作系统是真正的开始工作了,所以还要再次建立这个表
startup_32:
movl $0x10,%eax
mov %ax,%ds
mov %ax,%es
mov %ax,%fs
mov %ax,%gs ; 指向gdt的0x10项(数据段)
lss _stack_start,%esp ; 设置系统栈
还有其他的一些内容,这里不表。
### 三种汇编格式
我们可以看到,这里的汇编和之前的汇编代码的格式有点不同,因为现在是 32 位保护模式,用了 32 位的汇编。我们对汇编的格式做个简单的介绍
(1) as86 汇编:能产生 16 位代码的 Intel 8086(386)汇编
```asm
mov ds, ax, ; ax → ds, 目标操作数在前movl var, %eax ; (var) → eax
movb -4(%ebp), %al ; 取出一字节(3) 内嵌汇编,gcc 编译 x.c 会产生中间结果 as 汇编文件 x.s
__asm__(“汇编语句”
: 输出
: 输入
: 破坏部分描述);
//例如
__asm__(“movb
%%fs:%2, %%al” //%2表示addr,
:”=a”(_res) //a表示使用eax,并编号%0
:”0”(seg),”m”(*(addr)) //0或空表示使用与相应输出一样的寄存器 m表示使用内存
); 其实操作系统确实是一个复杂的工程,光汇编就用了 3 种:16 位汇编(bootsect.s 和 setup.s),32 位汇编(head.s),还有内嵌汇编(在后面讲到的 C 语言代码里,因为有些指令还是要精细的控制)。这里不展开讲这 3 种汇编的语法,这些不是本堂课的主线,如果等学完了三种汇编,黄花菜都凉了,因此待后续讲到的时候再简单的说一说和查找资料即可
当 head.s 执行完后,接下来就是执行 main.c 代码了。
after_page_tables:
pushl $0 ;These are the parameters to main :-)
pushl $0
pushl $0
pushl $L6 ; return address for main, if it decides to.
pushl $_main
jmp setup_paging
L6: jmp L6 ; main should never return here, but just in case, we know what happens.
setup_paging:
; …………这里省略一些设置页表代码……
ret
我们知道,汇编执行子程序的话,可以通过跳转指令;
C 语言执行函数(子程序)的话,用的是调用函数的语句,例如调用方法 b(假设需要传参),就用 `b(int a, int b)` 即可。但其实,,然后才被 CPU 执行。
因此,本段汇编代码的一开始几条压栈语句,就是传参给 CPU;然后将 main 函数的地址压到栈中;当 setup_paging 执行 ret 后,就回执行函数 main 了!(执行 ret 指令后,会将栈里的内容取出作为下一个要执行的代码的地址)
### 如果 main 返回了……
最后我们来看一个奇怪的语句:
```asm
L6: jmp L6 ; main should never return here, but just in case, we know what happens.这段代码不就是死循环吗?为什么要设置成死循环?
其实,操作系统是一个永远不停止的程序。如果一旦 main 函数停止了,就会跳转到这里,然后死循环,也就是我们常见的死机…………
head.s 做了什么?
代码路径:init/main.c
我们看一些关键的代码
void main(void)
{
// ........省略部分代码 ........
mem_init(main_memory_start,memory_end);
trap_init();
blk_dev_init();
chr_dev_init();
tty_init();
time_init();
sched_init();
buffer_init(buffer_memory_end);
hd_init();
floppy_init();
sti();
move_to_user_mode();
if (!fork()) {
init();
}
// ........省略部分代码 ........
}首先,为什么 main 函数的参数是 void? 其实三个参数分别是 envp,argv,argc,但目前版本的 main 没有使用,且我们在 head.s 里可以看到,在 push main 函数之前,都压栈了 3 个 0,所以这里是没有问题的
其次,我们可以看到,有很多的函数,并且都是带 init 字眼的,这些就是初始化内存,中断,时钟,硬盘,显示器等(Linux0.11 不支持鼠标)
每一个都可以说很久很久,我们这里简单说说 mem_init(),其他的都类似
mem_init() 顾名思义就是初始化内存的。前面我们提到操作系统就是管理硬件的,内存是一个重要的硬件,因此本函数就是初始化一些数据结构用来保存内存的信息,例如哪些被使用了,哪些是空闲的。
我们看看 mem_init() 的部分代码(在 linux-0.11\mm\memory.c 中 399 行)
#define USED 100
#define PAGING_PAGES (PAGING_MEMORY>>12)
int i;
HIGH_MEMORY = end_mem;
for (i=0 ; i<PAGING_PAGES ; i++)
mem_map[i] = USED;
i = MAP_NR(start_mem);
end_mem -= start_mem;
end_mem >>= 12;
while (end_mem-->0)
mem_map[i++]=0;
} 我们先看看参数,start_mem, end_mem 是 main 函数里传参的,而 main 函数是这样调用的:
static long main_memory_start = 0;
static long memory_end = 0;
//..........省略部分代码
memory_end = (1<<20) + (EXT_MEM_K<<10);
memory_end &= 0xfffff000;
//..........省略部分代码
mem_init(main_memory_start,memory_end);其实内存的大小,在 setup.s 里就已经存储到了 0x90000 处,通过 main 函数里读取,然后传个 mem_init() 函数。 接下来 mem_init() 里做什么呢?首先有一个全局变量 mem_map,里面的值如果是 0,表明内存没有被使用,如果是 100,表明是已经被使用了。
所以在 mem_init() 首先将 0 地址处(也就是自己 system 模块)的代码标记为已使用;然后将剩余的内存表明为未使用
我们可以画下操作系统在磁盘里的逻辑示意图:
第一个部分存放 bootsect.s,第二个部分存放 setup 模块,第三个部分存放 system 模块(大概占 240 个扇区)。这个顺序是不能变的,如果有一点点差错,都会死机。那么,操作系统是如何从源代码,编译成我们想要的样子呢?这就得提到 make file。
我们平时写 C 语言的时候,都是由 IDE 帮我们编译并运行的,不用关心程序运行在内存的哪里;而如果做操作系统,一切的事情都要自己控制。除了要写源码之外,还要确定如何编译操作系统生成镜像(这里镜像可以简单理解为操作系统安装包,英文名 Image),也就是 make file
初步讲下如何确保磁盘里的第一段代码是 bootsect,第二段是 setup 呢?通过 makefile,并且有很多依赖文件,例如 head.s,main.s,驱动等等。把这些汇编成.o 文件,然后链接起来生成 system。 我们可以看到 Linux 根目录下有个 Makefile 文件,我们可以看看里面的关键内容:
Image: boot/bootsect boot/setup tools/system tools/build
tools/build boot/bootsect boot/setup tools/system $(ROOT_DEV) > Image我们略略说一下,最后操作系统镜像 Image,是依赖于 bootsect 的,还依赖于 setup,system,还有很多工具类(tools),最后将这些代码链接起来,生成镜像,然后就可以运行这个镜像了。
仅仅在内存中加载了这些代码模块,并不能让 Linux 运行起来。完整可运行的 Linux 还需要一个基本的文件系统支持,即根文件系统。Linux0.11 内核仅支持 MINIX 的 1.0 文件系统。在 bootsect.s 的第 43 行给出了根文件系统所在的默认块设备号,更多知识我们后续再讲。
我们总结下这堂课:
图 6-2 清晰地显示出 Liux 系统启动时这几个程序或模块在内存中的动态位置。其中,每一竖条框代表某一时刻内存中各程序的映像位置图。先看图例
我们本堂课主要讲了引导扇区的代码做了什么事,其实我们可以换一个角度想想,bootsect.s 应该做什么?
在刚一上电的时候,操作系统一定还在硬盘上,而计算机的工作原理是取指执行,因此就要运行操作系统,就必须将操作系统读到内存里。所以 bootsect.s 应该就是将操作系统从磁盘读入到内存里。
当操作系统读入到内存后,操作系统就要管理硬件,因此需要初始化,读取硬件的参数,形成一些关键重要的数据结构来管理内存(例如 mem_map 管理内存)
所以,计算机在启动的时候就做了两件事,第一件事情把操作系统读到内存里,而第二步就是初始化操作系统运行需要的一些参数。
为什么 bootsect.s 会被加载到 0x7C00?
这个地址来源于 IBM 的 PC 5150 BIOS 开发团队,该团队最早开发了 DOS 1.0 的相关设计。0x7C00 其实是数值上等于 31KB,而最初的 DOS 1.0 最少需要 32KB 的 RAM。设计团队想给 OS 在这 32KB 内留下足够的空间用于 OS 加载,而加载程序需要 512 个字节。这样,团队选择了 32KB 中的最后 1KB 用来加载 OS。一旦 OS 加载之后,这部分 RAM 其实是可以继续让 OS 使用的。
那为什么后续到了其他的应用中依然还用这个地址呢?很简单了,肯定是为了各种兼容而存在的了。
操作系统(哈工大李治军老师)32 讲(全) 第 2 课,第 3 课
《Linux 内核完全剖析基于 0.11 内核》第 6.1 节 (本书好像已经不再发版了,当当上都是二手书。)
Linux v0.11 源码:http://oldlinux.org/Linux.old/Linux-0.11/sources/system/linux-0.11.tar.Z
bootsect.s,setup.s 和 head.s 的路径:linux-0.11\boot\。该目录下也就只有这几个文件